package com.service.transaction;

import com.container.app.annotations.Inject;
import com.container.app.annotations.ServiceComponent;
import com.container.app.proxy.ContainerProxyHandler;
import com.service.annotation.ConnectionDependent;
import com.service.annotation.Transactional;
import com.service.exceptions.BusinessServiceUncheckedException;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * Created with IntelliJ IDEA.
 * User: Georgiana.Scarlat
 * Date: 7/30/12
 * Time: 3:19 PM
 */
@ServiceComponent
public class TransationalProxyHandler extends ContainerProxyHandler {

    private static TransactionManager transactionManager;

    @SuppressWarnings("UnusedDeclaration")
    @Inject
    public static void setTransactionManager(TransactionManager atransactionManager) {
        transactionManager = atransactionManager;
    }

    protected TransationalProxyHandler(Object proxiesObject) {
        super(proxiesObject);
    }

    @Override
    public void beforeMethod(Method method) {
        if (isMethodAnnotatedWith(method, ConnectionDependent.class) || isMethodAnnotatedWith(method, Transactional.class))
            transactionManager.startConnection();
    }

    @Override
    public void afterMethod(Method method) {
        if (isMethodAnnotatedWith(method, ConnectionDependent.class) || isMethodAnnotatedWith(method, Transactional.class))
            transactionManager.endConnection();
    }

    @Override
    public Object wrapMethod(Method method, Object[] args) {
        Object result;
        boolean isTransactional = isMethodAnnotatedWith(method, Transactional.class);
        Transaction transaction;

        beforeMethod(method);

        transaction = doTransactionOperation(isTransactional, new Transaction(method.getName()), TransactionOperation.START);

        try {

            result = method.invoke(getProxiesObject(), args);

            doTransactionOperation(isTransactional, transaction, TransactionOperation.COMMIT);

            return result;

        } catch (IllegalAccessException e) {

            doTransactionOperation(isTransactional, transaction, TransactionOperation.ROLLBACK);

            throw new BusinessServiceUncheckedException("Method " + method.getName() + " can't be accessed.", e);
        } catch (InvocationTargetException e) {

            doTransactionOperation(isTransactional, transaction, e.getTargetException());

            throw new BusinessServiceUncheckedException("Method " + method.getName() + " has thrown an exception: " + e.getTargetException().getMessage() + ".", e);

        } finally {

            afterMethod(method);
        }

    }

    private Transaction doTransactionOperation(boolean isTransactional, Transaction transaction, Throwable targetException) {

        if(transactionManager.getRollbackExceptions().contains(targetException.getClass())) {
            return doTransactionOperation(isTransactional,transaction,TransactionOperation.ROLLBACK);
        }
        return null;
    }


    private Transaction doTransactionOperation(boolean isTransactional, Transaction transaction, TransactionOperation type) {
        Transaction trans = null;
        if (isTransactional) {
            switch (type) {

                case ROLLBACK:
                    transactionManager.rollBack(transaction);
                    break;
                case COMMIT:
                    transactionManager.commit(transaction);
                    break;
                case START:
                    trans = transactionManager.getTransaction(TransactionType.DEFAULT, transaction != null ? transaction.getName() : null);
                    break;
            }
        }
        return trans;

    }

    private boolean isMethodAnnotatedWith(Method method, Class<? extends Annotation> annotationClass) {

        if (isProxiedObjectMethodAnnotatedWith(method, annotationClass))
            return true;

        if (method.isAnnotationPresent(annotationClass))
            return true;

        Class declaringClass = method.getDeclaringClass();

        return declaringClass.isAnnotationPresent(annotationClass);
    }

    private boolean isProxiedObjectMethodAnnotatedWith(Method method, Class<? extends Annotation> annotationClass) {
        if (getProxiesObject().getClass().isAnnotationPresent(annotationClass))
            return true;
        Method methods[] = getProxiesObject().getClass().getDeclaredMethods();

        for (Method m : methods) {
            if (equalMethods(m, method) && m.isAnnotationPresent(annotationClass))
                return true;
        }

        return false;
    }

    private boolean equalMethods(Method m, Method method) {

        return m.getName().equals(method.getName()) && m.getReturnType().equals(method.getReturnType()) && Arrays.equals(m.getParameterTypes(), method.getParameterTypes());

    }


}
